#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import os, os.path
import pango
import gobject

import gtk
from gtk import glade

import ftp
import icons
import connectdialog
import about
import app
from gtkmisc import *


class BrowserStore(gtk.TreeStore):
    I_FILENAME = 0
    I_SIZE = 1
    I_ITEM = 2
    I_ACCESSIBLE = 3
    
    def __init__(self):
        gtk.TreeStore.__init__(self, str, str, object, bool)
        self.iters = {'': None}
        
    def _item_to_row(self, item):
        return [unic(item.filename), 
            item.isfile() and units(item.size) or "", 
            item, True]
        # TODO: We could probably do some checks to see if
        # the dir is accessible
        
    def add(self, path, item, overwrite=True):
        """Add item at path."""
        if not item.filename:
            item.filename = "(not set)"
        if path.startswith('/'): 
            path = path[1:]
        if not path in self.iters.keys():
            try:
                p, n = path.rsplit('/', 1)
            except ValueError:
                p = ""
                n = path
            self.add(p, ftp.RemoteFile(filename=n, type='d', dir=p, 
                server=item.server))
        
        parent_iter = self.iters[path]
        i = gtk.TreeStore.append(self, parent_iter,
            self._item_to_row(item))
        self.iters[(path and path + '/' or '') + item.filename] = i
    
    def dir_exists(self, d):
        """Make sure d exists."""
        if d.startswith('/'): d = d[1:]
        if not d in self.iters:
            if '/' in d:
                n, p = d.rsplit('/', 1)
            else:
                n, p = '', d
            diritem = ftp.RemoteFile(filename=p, type='d')
            self.add(n, diritem, overwrite=False)
            
    def clear(self, path=None):
        """Clear Store. If path is given, clear all entries under path."""
        if path == None:
            return gtk.TreeStore.clear(self)
        
        if path.startswith("/"): path = path[1:]
        path_iter = self.iters[path]
        if path_iter == None:
            # same as clearing the whole store
            return gtk.TreeStore.clear(self)
        else:
            for iterkey in self.iters.keys():
                if iterkey == '': continue
                if self.is_ancestor(path_iter, self.iters[iterkey]):
                    self.remove(self.iters[iterkey])
                    del self.iters[iterkey]
                    

def select_dir(title="Select destination directory"):
    selected = None
    chooser = gtk.FileChooserDialog(title=title,
        action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
        buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
    response = chooser.run()
    
    if response == gtk.RESPONSE_OK:
        selected = chooser.get_filename()
    chooser.destroy()
    
    return selected


class LogWindow:
    _widgets = ['textview_log', 'entry_cmd', 'button_clear', 'scroll_log']
    
    def __init__(self, parent_window):
        self.parent_window = parent_window
        
        self.wTree = gtk.glade.XML(app.glade_file, "log_window")
        for widget in self._widgets:
            setattr(self, widget, self.wTree.get_widget(widget))
        self.window = self.wTree.get_widget("log_window")
        
        _signals = {
            'entry_cmd_activate': self.entry_cmd_activate,
            'button_clear_clicked': self.button_clear_clicked,
        }
        self.wTree.signal_autoconnect(_signals)
        
        buff = self.textview_log.get_buffer()
        buff.create_tag("monospaced", family="Monospace")
        buff.create_tag("green", foreground="#00e000")
        buff.create_tag("blue", foreground="blue")
        buff.create_tag("red", foreground="red")
        buff.create_tag("bold", weight=pango.WEIGHT_BOLD)
        
        self.window.show_all()
        
    def entry_cmd_activate(self, widget):
        cmd = widget.get_text()
        if self.parent_window.ftp:
            if cmd.startswith("cd "):
                cmd, path = cmd.split(" ", 1)
                self.parent_window.cd(path)
            else:
                self.parent_window.ftp.quote(widget.get_text())
            widget.set_text('')
        else:
            app.debug("No FTP connection", "browser")        

    def button_clear_clicked(self, button):
        self.textview_log.get_buffer().set_text('')
        
    def log(self, message, type=0):
        buff = self.textview_log.get_buffer()
        scroll = self.scroll_log
        message = unic("%s\n" % message)
        
        tags = ["monospaced"]
        if type == 1: tags.append("green")
        elif type == 2: tags.append("blue")
        elif type == 3: tags.append("bold")
        elif type == 4:
            tags.append("bold")
            tags.append("red")
        
        end_iter = buff.get_end_iter()
        buff.insert_with_tags_by_name(end_iter, message, *tags)
        
        adj = scroll.get_vadjustment()
        adj.value = adj.upper
        scroll.set_vadjustment(adj)


class BrowserWindow:
    _widgets = ['textview_log', 'statusbar', 'scroll_log', 'scroll_files',
        'checkitem_io', 'checkitem_hidden']
    
    def __init__(self, appinst):
        self.appinst = appinst
        self.config = appinst.config
        
        self.wTree = gtk.glade.XML(app.glade_file, "browser_window")
        # get widgets
        for widget in self._widgets:
            setattr(self, widget, self.wTree.get_widget(widget))
        self.window = self.wTree.get_widget('browser_window')
        self.tree = self.wTree.get_widget('tree_files')
        
        # connect signals
        _signals = {
            'tree_files_row_activated': self.tree_row_activate,
            #'tree_files_button_release_event': self.tree_button_event,
            'tree_files_button_press_event': self.tree_button_event,
            'tree_files_cursor_changed': self.tree_cursor_changed,
            'tree_files_drag_data_received': self.drag_data_recv,
            'browser_window_destroy': 
                lambda w: self.appinst.close_window(self),
            'imagemenuitem_close_activate': 
                lambda w: self.window.destroy(),
            'imagemenuitem_connect_activate': 
                lambda w: self.new_connect_dialog(),
            'menuitem_new_activate':
                lambda w: self.appinst.new_browser(),
            'checkitem_hidden_toggled':
                lambda w: self.config.set("browser", 
                "show_hidden_files", str(w.get_active())),
            'menuitem_about_activate':
                lambda w: about.AboutDialog().run(),
            'menuitem_new_queue_activate':
                lambda q: self.appinst.new_queue(),
            'menuitem_preferences_activate':
                lambda w: self.appinst.new_preferences(),
            'menuitem_log_activate': self.menuitem_log_activate,
            'browser_window_check_resize': self.window_resized,
        }
        self.wTree.signal_autoconnect(_signals)
        
        self.log_window = None
        
        # enable multi-select in treeview
        self.tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
        
        # set up the treeview
        self.store = BrowserStore()
        self.tree.set_model(self.store)
        
        cell_icon = gtk.CellRendererPixbuf()
        cell_name = gtk.CellRendererText()
        
        column_name = gtk.TreeViewColumn("Name")
        column_name.pack_start(cell_icon, False)
        column_name.pack_start(cell_name, True)

        column_name.set_cell_data_func(cell_icon, self.tree_get_icon)
        column_name.set_attributes(cell_icon)
        column_name.set_attributes(cell_name, text=BrowserStore.I_FILENAME)
        column_name.set_expand(True)
        column_name.set_max_width(0)
        column_name.set_sort_column_id(BrowserStore.I_FILENAME)
        
        self.column_name = column_name
        
        self.tree.append_column(column_name)
        self.tree.set_enable_search(True)
        self.tree.set_headers_visible(False)
        
        # set up drag and drop on the tree
        self.tree.enable_model_drag_dest([('text/plain', 0, 0)], 
            gtk.gdk.ACTION_DEFAULT)
        
        # resize window
        self.window.set_default_size(
            self.config.getint("browser", "width"),
            self.config.getint("browser", "height")
        )
        
        self.in_auto = False # True will turn off listing and pwding 
                             # in browsers callbacks
        self.ftp = None
        self.homewd = ""
        self.window.set_icon_from_file(os.path.join(
            app.data_dir, "flash-64.png"))
        self.checkitem_hidden.set_active(
            self.config.getboolean("browser", "show_hidden_files"))
        self.window.show_all()
        self.tree.set_sensitive(False)
        
    def window_resized(self, window):
        """The window has been resized. Save new dimensions."""
        width, height = window.get_size()
        self.config.set("browser", "width", str(width))
        self.config.set("browser", "height", str(height))

    
    def close(self):
        """Close window."""
        if self.ftp and self.ftp.sock:
            self.disconnect()
        self.window.destroy()
        
    def new_connect_dialog(self):
        """Creates a new connect dialog for this browser."""
        cd = connectdialog.ConnectDialog(self.appinst, browser=self)
        cd.window.run()
        
    def cd(self, location):
        """Change current directory to location."""
        self.tree.set_sensitive(False)
        self.ftp.cwd(location)
        
    def drag_data_recv(self, widget, dc, x, y, selection, info, ts):
        files = []
        for ld in selection.data.split("\r\n"):
            if ld.startswith("file://"): files.append(ld[7:])
        if not files:
            app.debug("No files dropped")
            return
        
        drop_info = self.tree.get_dest_row_at_pos(x, y)
        if drop_info:
            path, position = drop_info
            i = self.store.get_iter(path)
            dropped_on = self.store.get_value(i, BrowserStore.I_ITEM)
            if dropped_on.isdir():
                dropped_on_dir = dropped_on.abs_location()
            elif dropped_on.isfile():
                dropped_on_dir = dropped_on.dir
            app.debug("%s dropped on %s" % (files, dropped_on_dir))
            
            if not self.appinst.queues:
                app.debug("No queues to handle drop")
                return
            
            que = self.appinst.queues[0] # XXX
            for f in files:
                rf = ftp.RemoteFile(dir=dropped_on_dir, server=dropped_on.server,
                    filename=os.path.split(f)[1], size=os.path.getsize(f))
                que.append_item(f, rf)
            
    def tree_get_icon(self, column, cell, model, iter):
        """Data function for the file icon in self.tree."""
        item = model.get_value(iter, BrowserStore.I_ITEM)
        try:
            if item.isdir():
                if self.tree.row_expanded(self.store.get_path(iter)):
                    pixbuf = icons.folder(open=True)
                else:
                    pixbuf = icons.folder()
            elif item.islink():
                pixbuf = icons.from_theme('emblem-symbolic-link')
            else:
                pixbuf = icons.by_filename(item.filename)
            cell.set_property('pixbuf', pixbuf)
        except gobject.GError, e:
            app.debug("Could not load pixbuf: " + str(e), "browser")
            cell.set_property('pixbuf', None)
        
    def tree_row_activate(self, widget, path, column):
        item = self.store.get_value(
            self.store.get_iter(path), BrowserStore.I_ITEM
        )
        # change directories if directory
        if item.isdir() or item.islink():
            self.set_status("Changing Directory")
            self.cd(item.abs_location())
        elif item.isfile():
            app.debug("No action implemented", "browser")
        else:
            app.debug("Unknown file", "browser")
            
    def tree_cursor_changed(self, widget):
        """Selection changed, update statusbar info."""
        iters = iters_selection(self.tree.get_selection())
        items = [self.store.get_value(x, BrowserStore.I_ITEM) for x in iters]
        if len(items) == 0: return
        elif len(items) == 1:
            self.set_status("%s, %s" % (items[0].filename, 
                items[0].isdir() and "(folder)" or units(items[0].size)))
        else:
            self.set_status("%s items, %s" % (len(items),
                str(units(sum([item.size for item in items])))
                ))
            
    def tree_button_event(self, widget, event):
        """Mouse button pushed over tree. Unclear code is unclear."""
        iters = iters_selection(self.tree.get_selection())
        if len(iters) == 0:
            return
        else:
            selected = [self.store.get_value(i, BrowserStore.I_ITEM) for i in iters]
            
        def download(items, queue_index=None, download_dir=''):
            """Add items to a queue. If there are no queues, create a new one."""
            if not self.appinst.queues:
                self.appinst.new_queue()
            if queue_index == None:
                queue_index = 0
            for item in items:
                if item.isfile():
                    self.appinst.queues[queue_index].append_item(item, 
                        os.path.join(download_dir, item.filename))
                elif item.isdir():
                    self.tree.set_sensitive(False)
                    self.in_auto = True
                    self.ftp.walk(
                        item.abs_location(), 
                        queue_number=queue_index,
                        started_path=item.dir,
                        target_dir=download_dir
                    )
        
        # popup menu in right click (button 3)
        if event.button == 3:    
            menu_popup = gtk.Menu()
            
            if selected[0].isdir() or selected[0].islink():
                go = gtk.ImageMenuItem("List Directory")
                go.set_image(gtk.image_new_from_stock(
                    "gtk-open", gtk.ICON_SIZE_MENU))
                go.connect('activate', lambda w: self.cd(selected[0].abs_location()))
                menu_popup.append(go)
            
            def download_if_selected(w, i, q):
                dir = select_dir()
                if dir:
                    download(i, download_dir=dir, queue_index=q)
            
            if len(self.appinst.queues) == 0:
                it = gtk.ImageMenuItem("Add to new Queue")
                it.connect('activate', lambda w: 
                    (self.appinst.new_queue(name=self.server.hostname), 
                    download_if_selected(None, selected, 0)))
            if len(self.appinst.queues) == 1:
                it = gtk.ImageMenuItem("Add to Queue")
                it.connect('activate', download_if_selected, selected, 0)
            elif len(self.appinst.queues) > 1:
                it = gtk.ImageMenuItem("Add to Queue")
                menu_queues = gtk.Menu()
                for i, q in enumerate(self.appinst.queues):
                    foo = gtk.MenuItem(q.name)
                    foo.connect('activate', download_if_selected, selected, i)
                    menu_queues.append(foo)
                it.set_submenu(menu_queues)
            it.set_image(gtk.image_new_from_stock("gtk-save-as", gtk.ICON_SIZE_MENU))
            
            menu_popup.append(it)
            
            d = gtk.ImageMenuItem("Delete")
            d.set_image(gtk.image_new_from_stock("gtk-delete", gtk.ICON_SIZE_MENU))
            d.connect("activate", lambda w: [self.ftp.delete(x) for x in selected])
            menu_popup.append(d)
            
            r = gtk.ImageMenuItem("Rename")
            r.set_image(gtk.image_new_from_stock("gtk-rename", gtk.ICON_SIZE_MENU))
            menu_popup.append(r)
            
            menu_popup.show_all()
            menu_popup.popup(None, None, None, event.button, event.time)
        
    def log(self, *args, **kwargs):
        if self.log_window:
            self.log_window.log(*args, **kwargs)
        
    def connect(self, server):
        """Connect to FTP server."""
        if self.ftp and self.ftp.sock:
            self.disconnect()
            
        self.ftp = ftp.FTPThread(daemon=True)
        _callbacks = {}
        for cb in ftp.FTPThread._all_callbacks:
            if hasattr(self, "ftp_%s" % cb):
                _callbacks[cb] = getattr(self, "ftp_"+cb)
        self.ftp.add_callbacks(_callbacks)
        
        self.set_status("Connecting")
        self.log("Connecting to ftp://%s:%s@%s:%s" % \
                    (server.username, 
                    len(server.password)*"*", 
                    server.hostname, 
                    server.port or "21"))
        
        self.ftp.connect(server)
        
        self.server = server
        
    def disconnect(self):
        if self.ftp and self.ftp.sock:
            self.ftp.quit()
            self.ftp.join()
        else:
            app.debug("disconnect() called, spite no connection.", "browser")
        
    def set_status(self, message):
        context_id = self.statusbar.get_context_id("status")
        self.statusbar.pop(context_id)
        self.statusbar.push(context_id, message)
        
    def menuitem_log_activate(self, widget):
        if not self.log_window:
            self.log_window = LogWindow(self)
        
    @threads
    def ftp_connected(self, welcomemsg):
        self.set_status("Connected to %s" % self.server.hostname)
        self.window.set_title("Flash! %s" % self.server.hostname)
        self.ftp.login()
    
    @threads    
    def ftp_connect_error(self, msg):
        try:
            if len(msg[1]) > 1:
                # in many cases, we get a typle with 
                # errorid, and explaination string
                msg = msg[1]
        except: pass
        self.set_status("Could not connect to %s: %s" % \
            (self.server.hostname, str(msg)))
        self.log("Could not connect: %s" % str(msg), type=4)

    @threads
    def ftp_sending(self, data):
        app.debug(data, "browser-ftp")
        if data.startswith("PASS"):
            data = "PASS <hidden>"
        self.log(data, type=2)
        
    @threads
    def ftp_received(self, data):
        app.debug(data, "browser-ftp")
        self.log(data, type=1)
    
    @threads    
    def ftp_authenticated(self, r):
        self.set_status("Connected, Authenticated")
        self.in_auto = True
        self.ftp.pwd() # the ftp session know about current directory
        self.ftp.list_parents() # go down and list
    
    @threads    
    def ftp_authentication_error(self, e):
        self.set_status("Not Authenticated")
        self.log("Could not authenticate: " + str(e), type=4)
    
    @threads    
    def ftp_disconnected(self, msg):
        self.store.clear()
        self.tree.set_sensitive(False)
        
    @threads    
    def ftp_pwd(self, wd):
        app.debug("wd=" + wd + ", listing.", "browser")
        self.set_status("Current directory: %s"%wd)
        self.store.dir_exists(wd)
        if not self.in_auto:
            self.set_status("Retreiving Directory Listing")
            self.ftp.ls()
        if wd[1:]:
            # activate this row
            store_path = self.store.get_path(self.store.iters[wd[1:]])
            self.tree.set_cursor(store_path)
    
    def ftp_cwd(self, msg):
        if self.in_auto: return
        self.ftp.pwd()
    
    @threads    
    def ftp_cwd_error(self, msg):
        self.tree.set_sensitive(True)
        self.set_status("Could not change directory: %s" % msg)
    
    @threads
    def ftp_listing(self, listing):
        show_hidden = self.config.getboolean('browser', 'show_hidden_files')
        self.store.clear(self.ftp.wd)
        for item in listing:
            if item.ishidden() and not show_hidden: continue
            self.store.add(self.ftp.wd, item)
        
        if self.ftp.wd[1:]:
            store_path = self.store.get_path(self.store.iters[self.ftp.wd[1:]])
            self.tree.expand_row(store_path, True)
        if not self.in_auto:
            self.tree.set_sensitive(True)
    
    @threads
    def ftp_timeout(self, e):
        self.log("Timeout: " + str(e), type=4)
    
    @threads
    def ftp_unhandled_exception(self, job, exception):
        self.log("Unhandled exception in FTP thread: Job: %s Exception: %s" %
            (job, exception), type=4)
            
    @threads
    def ftp_ssl_enabled(self, server, issuer):
        self.log("SSL has been enabled")
        
    @threads
    def ftp_deleted(self, filename):
        self.set_status("%s deleted" % filename)
        # XXX: Remove correct row!
        
    def ftp_walk_end(self):
        self.in_auto = False
            
    @threads
    def ftp_ssl_not_availible(self, s):
        self.log("SSL not availible.", type=4)
        self.set_status("No SSL")
    
    @threads
    def ftp_list_parents_finished(self):
        self.in_auto = False
        self.tree.set_sensitive(True)
    
    @threads
    def ftp_walk(self, i, queue_number=None, started_path=None, target_dir=None):
        target = os.path.join(target_dir, i.filename)
        app.debug("Walked %s" % i, "browser")
        if started_path:
            target = os.path.join(target_dir, i.abs_location()[len(started_path)+1:])
            app.debug("Putting in %s" % target, "browser")
        if i.isfile():
            self.appinst.queues[queue_number].append_item(i, target)
        
